package org.btallarm.json;


import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.json.JSONTokener;

public class DataConverter {

	/**
	 * Restituisce, se presente, l'annotazione StructInfo associata alla classe passata come parametro
	 * @param typeClass Classe da esaminare
	 * @return Annotazione StructInfo relativa alla classe passata come parametro
	 * @throws IllegalArgumentException nel caso la classe non contenga l'annotazione richiesta
	 */
	public static <T> JSONMappingInfo getJSONMappingInfoAnnotation(Class<T> typeClass) throws IllegalArgumentException {
		if (!typeClass.isAnnotationPresent(JSONMappingInfo.class)) {
			throw new IllegalArgumentException("Il tipo " + typeClass.getName()
					+ " passato come parametro non contiene l'annotazione " +
							"StructInfo necessaria per la conversione!");
		}
		return typeClass.getAnnotation(JSONMappingInfo.class);
	}

	private static Object getValueFromJSONSimpleField(JSONObject src, String fieldName, JSONMappingType jsonMappingType) throws JSONException {
		if(!src.isNull(fieldName)){
			Object retValue = null;//createNewObjectInstance(getEquivalentClass(jsonMappingType));
			
			switch (jsonMappingType){
				
				case JSON_INTEGER:{
					retValue = new Integer (src.getInt(fieldName));
					break;
				}
				
				case JSON_LONG:{
					retValue = new Long (src.getLong(fieldName));
					break;
				}
				
				case JSON_DOUBLE:{
					retValue = new Double (src.getDouble(fieldName));
					break;
				}
				
				case JSON_STRING:{
					retValue = new String (src.getString(fieldName));
					break;
				}
				
				case JSON_BOOLEAN:{
					retValue = new Boolean (src.getBoolean(fieldName));
					break;
				}
				
			}	
			return retValue;
		}
		return null;
	}

	
	private static <T> T createNewObjectInstance(Class<T> typeClass) throws IllegalArgumentException {
		T objectInstance;
		try {
			objectInstance = typeClass.newInstance();
			return objectInstance;
			
		} catch (InstantiationException e) {
			throw new IllegalArgumentException("Il tipo " + typeClass.getName() + " e' un'interfaccia o una classe astratta e " +
			"non puo' essere istanziato!");
			
		} catch (IllegalAccessException e) {
			throw new IllegalArgumentException("L'oggetto di tipo " + typeClass.getName() + " non dispone di costruttore pubblico!");
		}
	}
	
	private static Class<?> getEquivalentClass(JSONMappingType type){
		switch (type) {
			case JSON_STRING:
				return String.class;
			case JSON_BOOLEAN:
				return Boolean.class;
			case JSON_DOUBLE:
				return Double.class;
			case JSON_INTEGER:
				return Integer.class;
			case JSON_LONG:
				return Long.class;
			default:
				return null;
		}
	}
	
	public static void fromJSONObject(Object converted, JSONObject src) throws JSONException, NotAnnotationDeclaredException, InstantiationException, IllegalAccessException{
		for (Field field : converted.getClass().getDeclaredFields()) {
			String fieldName = field.getName();
			JSONMappingInfo fieldInfo = field.getAnnotation(JSONMappingInfo.class);
			if(!src.isNull(fieldName)){
				switch (fieldInfo.objectType()){				
					case JSON_OBJECT:{
						Object subObject = createNewObjectInstance(field.getType());
						JSONObject subJSONobject = src.getJSONObject(fieldName);
						fromJSONObject(subObject, subJSONobject);
						invokeSetter(converted, field, subObject, field.getType());
						break;
					}
					
					case JSON_ARRAY:{
						if(field.isAnnotationPresent(ArrayField.class)){
							ArrayField annotation = field.getAnnotation(ArrayField.class);
							JSONArray array = src.getJSONArray(fieldName);
							int arrayLength = array.length();
							Object subObject[] = null;
							Class arrayElementClass = field.getType().getComponentType();
							
							subObject = (Object[]) Array.newInstance(arrayElementClass, arrayLength);
							
							for(int i = 0; i < arrayLength ; i++){
								subObject[i] = null;
								if(annotation.enclosingType() == JSONMappingType.JSON_ARRAY || annotation.enclosingType() == JSONMappingType.JSON_OBJECT){							
									subObject[i] = arrayElementClass.newInstance();
									fromJSONObject(subObject[i], array.getJSONObject(i));
								}
								else{
									subObject[i] = getValueOfJSONArrayFromSimpleField(array, i, annotation.enclosingType());
								}
							}
							invokeSetter(converted, field, subObject, subObject.getClass());
						}
						else 
							throw new NotAnnotationDeclaredException();
						break;
					}
					
					default:{
						JSONMappingType type = fieldInfo.objectType();
						Object value = getValueFromJSONSimpleField(src, fieldName, fieldInfo.objectType());
						invokeSetter(converted, field, value, getEquivalentClass(type));
					}
				}
			}
		}
	}
	
	private static Object getValueOfJSONArrayFromSimpleField(JSONArray dest, int index, JSONMappingType type) throws JSONException {
		switch (type) {
		case JSON_BOOLEAN:
			return  new Boolean(dest.getBoolean(index));
		case JSON_INTEGER:
			return  new Integer(dest.getInt(index));
		case JSON_LONG:
			return  new Long(dest.getLong(index));
		case JSON_STRING:
			return  new String(dest.getString(index));
		case JSON_DOUBLE:
			return  new Double(dest.getDouble(index));	
		}
		return null;
		
	}

	public static void fromStream(String io, Object converted) throws JSONException, NotAnnotationDeclaredException, InstantiationException, IllegalAccessException{
		JSONObject jsonParsed = new JSONObject(new JSONTokener(io));
		fromJSONObject(converted, jsonParsed);
	}

	public static String toStream(Object toConvert) throws JSONException, NotAnnotationDeclaredException, IllegalArgumentException, IllegalAccessException{
		JSONObject jsonParsed = new JSONObject();
		toJSONObject(toConvert, jsonParsed);
		return jsonParsed.toString();
	}
	
	private static Object invokeGetter(Object object, Field field) throws IllegalArgumentException {
		Object tmpObject;
		
		try {
			String methodName = "get" + Character.toUpperCase(field.getName().charAt(0)) + field.getName().substring(1);
			Method tmpMethod = object.getClass().getMethod(methodName, (Class[]) null);
			
			tmpObject = tmpMethod.invoke(object, (Object[]) null);
			
			if (tmpObject == null) {
				//throw new IllegalArgumentException("Il campo " + field.getName() + " dell'oggetto " + object.getClass().getName() +
				//		" e' null e pertanto non puo' essere convertito!");
			}
			
			return tmpObject;

		} catch (SecurityException e) {
			throw new IllegalArgumentException("L'oggetto di tipo " + object.getClass().getName()
					+ " passato come parametro non permette l'accesso al campo"
					+ field.getName() + ". Controllare che sia stato dichiarato come pubblico!");

		} catch (NoSuchMethodException e) {
			throw new IllegalArgumentException("L'oggetto di tipo " + object.getClass().getName()
					+ " passato come parametro non ha metodo getter per il campo "+ field.getName() + "!");

		} catch (IllegalAccessException e) {
			throw new IllegalArgumentException("L'oggetto di tipo " + object.getClass().getName() 
					+ " passato come parametro non ha getter pubblico per il campo" + field.getName() + "!");
			
		} catch (InvocationTargetException e) {
			throw new IllegalArgumentException("Il metodo getter per il campo"
					+ field.getName() + " del tipo " + object.getClass().getName() + 
			"ha generato un'eccezione! Controllare la sua implementazione");
		}
	}

	
	public static void toJSONObject(Object toConvert, JSONObject dest) throws NotAnnotationDeclaredException, IllegalArgumentException, IllegalAccessException, JSONException {
		for (Field field : toConvert.getClass().getDeclaredFields()) {
			String fieldName = field.getName();
			JSONMappingInfo fieldInfo = field.getAnnotation(JSONMappingInfo.class);
			switch (fieldInfo.objectType()){				
				case JSON_OBJECT:{
					JSONObject subJSONobject = new JSONObject();
					Object subObject = invokeGetter(toConvert, field);
					if(subObject == null)
						dest.put(fieldName, JSONObject.NULL);
					else {
						toJSONObject(subObject, subJSONobject);
						dest.put(fieldName, subJSONobject);
					}
					break;
				}
				
				case JSON_ARRAY:{
					if(field.isAnnotationPresent(ArrayField.class)){
						ArrayField annotation = field.getAnnotation(ArrayField.class);
						
						JSONArray array = new JSONArray();
						Object []objArray = (Object[]) invokeGetter(toConvert, field);
						if(objArray == null){
							dest.put(fieldName, JSONObject.NULL);
						}
						else{
							int arrayLength = objArray.length;
							for(int i = 0; i < arrayLength ; i++){
								if(annotation.enclosingType() == JSONMappingType.JSON_ARRAY || annotation.enclosingType() == JSONMappingType.JSON_OBJECT){
									JSONObject obj = new JSONObject();								
									toJSONObject(objArray[i], obj);
									array.put(i, obj);								
								}
								else{
									setValueOfJSONArrayWithSimpleField(array, objArray[i], i, annotation.enclosingType());
								}
								
							}
							dest.put(fieldName, array);
						}
					}
					else 
						throw new NotAnnotationDeclaredException();
					break;
					
				}
				
				default:{
					JSONMappingType type = fieldInfo.objectType();
					Object obj = invokeGetter(toConvert, field);
					if(obj == null)
						dest.put(fieldName, JSONObject.NULL);
					else
						setValueOfJSONSimpleField(dest, obj, fieldName, fieldInfo.objectType());
				}
			}
			
		}
		
	}

	private static void setValueOfJSONArrayWithSimpleField(JSONArray dest, Object object, int index, JSONMappingType objectType) throws JSONException {
		switch (objectType){
		
		case JSON_INTEGER:{
			Integer integerVal = (Integer) object;
			dest.put(index, integerVal.intValue());
			break;
		}
		
		case JSON_LONG:{
			Long longVal = (Long) object;
			dest.put(index, longVal.longValue());
			break;
		}
		
		case JSON_DOUBLE:{
			Double doubleVal = (Double) object;
			dest.put(index, doubleVal.doubleValue());
			break;
		}
		
		case JSON_STRING:{
			String stringVal = (String) object;
			dest.put(index, stringVal);
			break;
		}
		
		case JSON_BOOLEAN:{
			Boolean booleanVal = (Boolean) object;
			dest.put(index, booleanVal.booleanValue());
			break;
		}	
	}
		
	}
	private static void setValueOfJSONSimpleField(JSONObject dest, Object object, String fieldName, JSONMappingType objectType) throws JSONException {
			switch (objectType){
				
				case JSON_INTEGER:{
					Integer integerVal = (Integer) object;
					dest.put(fieldName, integerVal.intValue());
					break;
				}
				
				case JSON_LONG:{
					Long longVal = (Long) object;
					dest.put(fieldName, longVal.longValue());
					break;
				}
				
				case JSON_DOUBLE:{
					Double doubleVal = (Double) object;
					dest.put(fieldName, doubleVal.doubleValue());
					break;
				}
				
				case JSON_STRING:{
					String stringVal = (String) object;
					dest.put(fieldName, stringVal);
					break;
				}
				
				case JSON_BOOLEAN:{
					Boolean booleanVal = (Boolean) object;
					dest.put(fieldName, booleanVal.booleanValue());
					break;
				}	
			}	
	}

	private static <T> void invokeSetter(Object object, Field field, Object valueToSet, Class<T> valueToSetClass) throws IllegalArgumentException {

		try {	
			String methodName = "set" + Character.toUpperCase(field.getName().charAt(0)) + field.getName().substring(1);
			Class<?>[] parameterClasses = new Class<?>[1];
			parameterClasses[0] = valueToSetClass;
			Method tmpMethod = object.getClass().getMethod(methodName, parameterClasses);

			Object[] tmpArray = new Object[1];
			tmpArray[0] = valueToSet;
			tmpMethod.invoke(object, tmpArray);

		} catch (SecurityException e) {
			throw new IllegalArgumentException("L'oggetto di tipo " + object.getClass().getName()
					+ " passato come parametro non permette l'accesso al campo"
					+ field.getName() + ". Controllare che sia stato dichiarato come pubblico!");

		} catch (NoSuchMethodException e) {
			throw new IllegalArgumentException("L'oggetto di tipo " + object.getClass().getName()
					+ " passato come parametro non ha metodo setter per il campo "+ field.getName() + "!");

		} catch (IllegalAccessException e) {
			throw new IllegalArgumentException("L'oggetto di tipo " + object.getClass().getName() 
					+ " passato come parametro non ha setter pubblico per il campo" + field.getName() + "!");

		} catch (InvocationTargetException e) {
			throw new IllegalArgumentException("Il metodo setter per il campo"
					+ field.getName() + " del tipo " + object.getClass().getName() + 
			"ha generato un'eccezione! Controllare la sua implementazione");
		}
	}

}
